数据库并发控制与封锁

多个人同时操作数据库,怎么才能既快又对?

🎯 本节课你需要掌握的(按重要程度排序)
  • 事务的概念 + ACID 四大性质 必考
  • 4 种典型并发问题:丢失更新 / 脏读 / 不可重复读 / 幻象读 必考
  • 4 种隔离级别能解决哪些问题 —— 一张对照表 必考
  • X 锁(写锁)和 S 锁(读锁)的区别 必考
  • 封锁协议、死锁的概念 了解

🏦 先想象一个场景:

你和你妈共用一张银行卡,里面有 1000 元

同一秒钟,你在便利店刷卡买了 100 元零食,同时 你妈在另一家店刷了 200 元菜钱。

👉 账户里 应该 剩多少?很简单,700 元
但如果数据库没处理好,结果可能是 800、900,甚至还是 1000……

本章就是要解决这个问题:多人同时操作数据库时,怎样保证算得对?

一、为什么需要"事务"?重点

1.1 数据库为什么要支持"并发"?

数据库的最大特点就是 数据共享—— 一份数据,很多人都要用。

🚶 串行(一个一个来)

你查的时候,别人必须等。等你查完了,下一个人才能用。

❌ 慢,CPU 大部分时间在闲着

🏃‍♀️🏃 并发(一起上)

多个人同时操作数据库,效率高。

✅ 快,但是要解决"同时操作会不会出错"

🔑 一句话总结

并发 = 多人同时用 = 提高效率,但需要 "控制" 避免出错。

1.2 什么是事务(Transaction)?必考

📖 定义(背下来)

事务是 一组数据库操作的集合,这一组操作必须 "要么全部成功,要么全部失败"不允许只成功一半

🌰 用转账理解事务

张三给李四转 100 元,数据库里实际要做 两件事

① 张三账户:余额 - 100
② 李四账户:余额 + 100

这两步必须 "绑在一起"。如果第①步成功了、第②步失败了 —— 那 100 块就凭空消失了!

所以我们要把这两步做成 一个事务:要么两步都成功,要么两步都不做(一旦中间出问题,刚才的扣款也撤销)。

MySQL 中的事务三件套必考

SQL
START TRANSACTION;   -- ① 启动事务
-- 这中间写若干条 SQL ……
COMMIT;              -- ② 提交:所有操作正式生效
ROLLBACK;            -- ③ 回滚:撤销所有操作,回到事务开始之前
💡 简单记忆

COMMIT = 我做完了,请保存 ✅
ROLLBACK = 我中间出错了,请假装我没做过 ❌

1.3 事务的 ACID 四大性质必考

事务能"靠谱"地工作,是因为它必须满足 4 条性质,合起来叫 ACID。这是必考点,每个字母代表什么、什么意思,必须背下来。

A
原子性Atomicity

事务里的操作,要么全部做完,要么全部不做,不能做一半。

C
一致性Consistency

事务执行 前后,数据库都处于"合理"的状态(钱不能凭空多也不能凭空少)。

I
隔离性Isolation

多个事务同时跑,互相不能看到对方的中间状态,就像各自独占数据库一样。

D
持久性Durability

事务一旦提交,数据 永远不会丢。哪怕数据库立刻断电,重启后数据还在。

用转账例子讲透 ACID(这就是考点)

张三向李四转 100 元,假设有个事务:

SQL · 转账事务
START TRANSACTION;
UPDATE account SET R = R - 100 WHERE id = '张三';
UPDATE account SET R = R + 100 WHERE id = '李四';
COMMIT;
💬 用 ACID 逐条对照(这是答题模板)

A 原子性:两条 UPDATE 必须 都成功。如果第二条失败,要 ROLLBACK 撤销第一条,否则张三扣了钱、李四没收到 —— 钱凭空消失。

C 一致性:转账前后,张三 + 李四的总金额 不变。钱不能凭空出现,也不能凭空消失。

I 隔离性:如果同一时刻还有别的转账在跑,它们不能看到这个事务"扣了钱但还没加给李四"的中间状态

D 持久性:一旦 COMMIT,张三 -100、李四 +100 就 写入磁盘,哪怕下一秒服务器宕机,重启后数据还在。

⭐ 重点提醒

本章后面所有内容,本质上都是为了维护 ACID 中的"I(隔离性)"。

因为多个事务并发执行,最容易出问题的就是隔离性 —— 怎么让事务感觉不到彼此存在?这就要靠 隔离级别封锁机制

二、并发会带来什么问题?4 种"翻车"场景核心重点

当多个事务一起跑、隔离性没做好时,会出现 4 种典型问题必须背得滚瓜烂熟,因为这是考试核心。

2.1 问题一:丢失更新(Lost Update)

📖 一句话定义

两个事务同时改 同一条数据后写的先写的 给覆盖了 —— 先写的修改"凭空消失"。

🌰 通俗类比

你和你妈同时打开 Word 编辑同一份文件 —— 你改了第一段保存, 改了第二段保存。结果:她保存的版本里没有你改的第一段!

因为她保存时用的是"打开时的旧版本"覆盖掉了你的修改。

时序演示

账户初始余额 1000 元。事务 T1 想取 100,事务 T2 想取 200,正确结果应该是 700

时间事务 T1余额 R事务 T2
t1SELECT R = 10001000
t21000SELECT R = 1000
t3UPDATE R = 1000-100 = 900900
t4800 ❌UPDATE R = 1000-200 = 800
🔑 看明白了吗?

T2 在 t2 时刻拿到的是 1000(旧值),它不知道 T1 已经改成 900 了。等 t4 写回 800 时,T1 的扣款 100 元就丢了

2.2 问题二:脏读(Dirty Read)

📖 一句话定义

一个事务读到了 另一个事务还没提交 的数据。如果对方后来 回滚 了,那读到的就是"假数据"。

🌰 通俗类比

同事跟你说:"老板刚说要给我们涨工资 5000!" 你高兴地告诉了爸妈。
结果第二天老板说:"昨天那是开玩笑的。" —— 你已经传出去的消息变成了"假新闻"。

同事的"涨工资"还没正式生效(没 COMMIT),就被你看到了 —— 这就是脏读。

时序演示

时间事务 T1余额 R事务 T2
t1SELECT R = 10001000
t2UPDATE R = 900(未提交900
t3900SELECT R = 900 ⚠️ 读到了未提交的
t4ROLLBACK(撤销!)1000
t51000但 T2 还在用 R = 900 ❌
🔑 关键标志

读到的数据来自 "还没提交" 的事务(甚至最后回滚了)—— 这就叫"脏"。

2.3 问题三:不可重复读(Unrepeatable Read)

📖 一句话定义

同一个事务里,读了两次同一行数据,结果 不一样 —— 因为中间另一个事务把它改了 并且提交了

🌰 通俗类比

你早上看天气预报:"今天 25℃",决定不带外套出门。
中午再看:"今天 15℃" ——天气预报中途被气象局更新了。

同一件事(今天的天气)你看了两次,结果不一样

时序演示

时间事务 T1余额 R事务 T2
t1SELECT R = 1000(第一次读)1000
t21000UPDATE R = 800
t3800COMMIT(已提交!)
t4SELECT R = 800(第二次读 ≠ 1000 ❌)800
🔑 与"脏读"的关键区别

这次 T2 是 已经提交 的(注意 t3 那个 COMMIT),所以 T1 读到的 800 是 真实数据不是脏数据

但问题在于:T1 在同一个事务里 读两次结果不一样 —— 这就是"不可重复读"。

2.4 问题四:幻象读(Phantom Read)

📖 一句话定义

同一个事务里,用相同条件查两次,得到的 记录条数 不一样 —— 因为另一个事务 插入或删除 了符合条件的行。

🌰 通俗类比

你在班级群里数"今天来上课的人":第一次数 30 人。
转头又数一遍:变成 32 人 —— 因为有 2 个人刚才偷偷加群了。

条件没变(都是数群里的人),但 "人数" 变了,仿佛凭空多出来 2 个"幻影"。

时序演示

时间事务 T1记录数事务 T2
t1SELECT * WHERE R<1000 → 3 行3
t23INSERT 一条 R=800 的新记录
t34COMMIT
t4SELECT * WHERE R<1000 → 4 行 ❌4
⭐ 必考易混点

"不可重复读" vs "幻象读" —— 学生最容易混淆,老师最爱考这里!

👉 不可重复读同一行值变了(比如 1000 → 800)
👉 幻象读查询条件下的 行数变了(比如 3 行 → 4 行)

一句话:一个改了"内容",一个改了"数量"

2.5 4 种问题速记口诀必考

🎯 一句话记一种
  • 丢失更新 = "覆盖"(两人改一份,后写的覆盖先写的)
  • 脏读 = "偷读"(读到了别人没提交的中间数据)
  • 不可重复读 = "同行变值"(同一行的值,第二次读变了)
  • 幻象读 = "行数变化"(同一条件,行数变多/变少了)
随堂测试

某事务对学生表 WHERE 班级='1班' 查询了两次,第一次返回 30 行,第二次返回 32 行(中间有人新增了 2 个 1 班学生 并提交了)。这属于哪种问题?

A. 丢失更新
B. 脏读
C. 不可重复读
D. 幻象读

✅ 正确答案:D · 幻象读

判断思路:① 已提交 → 排除"脏读";② 行数变了(不是值变了) → 是"幻象读",不是"不可重复读"。

三、解决之道(一):隔离级别核心重点

3.1 4 种隔离级别(从低到高)必考

SQL 标准定义了 4 种隔离级别,它们能挡住的问题不一样。级别越高 → 越安全 → 但也越慢

级别 1(最低)READ UNCOMMITTED
读未提交 什么都不挡,4 种问题都可能发生。几乎不用
级别 2READ COMMITTED
读已提交 能挡住 脏读,但仍可能不可重复读、幻象读。
级别 3 · MySQL 默认 ⭐REPEATABLE READ
可重复读 能挡住 脏读 + 不可重复读,仍可能幻象读。
级别 4(最高)SERIALIZABLE
串行化 4 种问题全挡 ✅。但性能最差(事务被强制排队)。
⚠️ 重要原则

没有"最好"的级别,只有"最适合业务"的级别。
MySQL 默认 REPEATABLE READ(可重复读)—— 平衡安全和性能,大部分场景够用。

3.2 一张对照表打通全部考点必考

下面这张表 必须背下来,考试 100% 会出现。 表示"能避免该问题", 表示"挡不住"。

隔离级别 脏读 不可重复读 幻象读
READ UNCOMMITTED(读未提交)
READ COMMITTED(读已提交)
REPEATABLE READ(可重复读)⭐ 默认
SERIALIZABLE(串行化)
⭐ 速记口诀

"未提交什么都不挡 → 已提交挡脏读 → 可重复读再挡不可重复读 → 串行化全挡"

每升一级,多挡一种问题。从低到高刚好对应:脏读 → 不可重复读 → 幻象读。

设置隔离级别(语法,了解即可)了解

SQL
-- 查看当前隔离级别
SELECT @@transaction_isolation;

-- 设置隔离级别(4 选 1)
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;

💡 SESSION 表示只对当前会话生效;GLOBAL 表示对所有连接生效。下节实验课会动手操作。

四、解决之道(二):封锁机制重点

4.1 什么是封锁(Locking)?

📖 一句话定义

事务在操作数据前,先给数据 "加锁"。其他事务想动这份数据,必须 等锁释放

🌰 通俗类比

公共自习室的座位。你想坐 → 先把书包放上去("加锁")→ 别人就不能来坐 → 你走的时候把书包拿走("释放锁")→ 别人才能用。

MySQL 提供 两种封锁粒度(锁住多大一片):

🗂️ 表级锁

锁住 整张表

✅ 简单,开销小
❌ 并发度低(一锁整桌都不能用)

📌 行级锁

只锁住 用到的那几行

✅ 并发度高
❌ 开销大,可能死锁

💡 MySQL 的 InnoDB 引擎默认用 行级锁,性能好。

4.2 两种锁类型:X 锁 与 S 锁必考

X排它锁(写锁)

规则:加了 X 锁,只有自己能读和写,其他人连读都不行。

✋ 关键词:独占,谁也别碰

S共享锁(读锁)

规则:加了 S 锁,大家都能读,但谁都不能写(包括自己)。

🤝 关键词:共享读,但都不能写

锁兼容矩阵:能不能同时加?必考

已经有一种锁的情况下,能不能再加另一种?

已加锁 ↓ /要加 →
S 锁
X 锁
S 锁
✓ 可以
✗ 等待
X 锁
✗ 等待
✗ 等待
⭐ 一句话速记

"只有 S+S 才能并存" —— 只要涉及 X 锁,必冲突。

本质:读不冲突;写排斥一切(既排斥别人写,也排斥别人读)。

MySQL 加锁语法了解

SQL
LOCK TABLES account READ;    -- 给 account 表加 S 锁(读锁)
LOCK TABLES account WRITE;   -- 给 account 表加 X 锁(写锁)
UNLOCK TABLES;                -- 释放当前会话所有锁

4.3 封锁协议(三级递进)了解

"在什么时候加什么锁、什么时候释放" —— 这种约定就叫 封锁协议。教材分了三级,递进的。不需要背时序,只需要记住下面这张对照表

封锁协议 规则要点 丢失更新 脏读 不可重复读
一级封锁协议 写之前加 X 锁,事务结束才释放
二级封锁协议 在一级基础上:读之前加 S 锁,读完立刻释放
三级封锁协议 在一级基础上:读之前加 S 锁,事务结束才释放
💡 一句话理解三级协议

关键看 S 锁什么时候释放
① 一级:根本没 S 锁(只管写) → 防 丢失更新
② 二级:S 锁 读完即释 → 多防 脏读(持锁期间别人不能写)
③ 三级:S 锁 事务结束才释 → 多防 不可重复读(整个事务期间别人都不能改)

五、死锁了解即可

5.1 什么是死锁?

📖 一句话定义

两个或多个事务 互相等对方放锁,结果谁也动不了 —— 系统卡死。

🌰 通俗类比

两个人过独木桥,各从一头走到中间,都不肯让
A 等 B 退回去,B 等 A 退回去 —— 永远卡在那。

典型死锁场景

时间事务 T1事务 T2
t1锁定 学生表 ✅
t2锁定 成绩表 ✅
t3想锁 成绩表 → 等待 ⌛
t4想锁 学生表 → 等待 ⌛
t5永远等下去 💀永远等下去 💀

T1 拿着学生表的锁,想要成绩表的锁;T2 拿着成绩表的锁,想要学生表的锁 —— 互相等,谁都不让。

5.2 怎么避免死锁?了解

💡 三个常用办法(理解就行,不用背)

① 顺序加锁法:所有事务 按相同顺序 锁表。比如规定都先锁学生表、再锁成绩表 —— 就不会出现"互等"。

② 一次加锁法:把需要的锁 一次性全申请,要么全拿到要么都不拿。

③ 直接申请够大的锁:要更新就直接 X 锁,不要"先 S 锁后升级 X 锁" —— 升级过程容易死锁。

💬 实际中怎么处理?

MySQL 自己会 自动检测死锁,发现后会主动回滚一个事务,让另一个继续执行。所以你写代码时,主要做好 预防(按顺序加锁)就行。

六、本章小结

📋 三句话总结整章

🎯 核心逻辑链
  • 数据库要 并发(多人同时用)才能性能好 → 但并发会带来 4 种数据不一致问题
  • 事务 ACID 是目标;隔离级别 是策略(4 个级别防不同问题);封锁 是底层手段。
  • 选哪个级别、用哪种锁,本质都是在 "安全 ↔ 速度" 之间做权衡。

本章必考点回顾

⭐ 期末考点(按出现频率排)

  1. 4 种不一致性问题:识别 + 区分(重中之重,不可重复读 vs 幻象读 必考)
  2. 4 种隔离级别 vs 3 种问题的对照表(必背)
  3. 事务的 ACID 四大性质(简答题常考)
  4. X 锁、S 锁的区别 + 兼容矩阵
  5. 事务三件套:START TRANSACTION / COMMIT / ROLLBACK

课堂综合测验

第 1 题

事务 T1 修改了余额但还没提交,T2 读到了 T1 修改后的值。后来 T1 因故障回滚。这是?

A. 丢失更新
B. 脏读
C. 不可重复读
D. 幻象读

✅ 正确:B。读到了"未提交"+"对方还回滚了" = 脏读的标志。

第 2 题

能避免"不可重复读"的最低隔离级别是?

A. READ UNCOMMITTED
B. READ COMMITTED
C. REPEATABLE READ
D. SERIALIZABLE

✅ 正确:C。回头看那张对照表 —— READ COMMITTED 还挡不住不可重复读,REPEATABLE READ 才行(也是 MySQL 默认)。

第 3 题

T1 已经对表 R 加了 S 锁。下面哪个操作能 立刻成功

A. T2 给 R 加 X 锁
B. T2 修改 R
C. T2 给 R 加 S 锁
D. T1 自己修改 R

✅ 正确:C。S+S 是唯一兼容的组合(多人共读)。A、B 都涉及写,会被 S 锁挡住;D 中 T1 自己持 S 锁也不能写(S 锁只让读)。

🚀 下节课:上机实验

下节实验课,我们会实际打开两个 MySQL 窗口,亲手做以下事情:

  • 🔬 在不同隔离级别下,亲眼看到脏读、不可重复读是怎么发生的
  • 🔒 用 LOCK TABLES 制造阻塞场景
  • 📝 大量练习题,巩固 4 种问题的辨认

请提前确保 MySQL 装好,能正常登录。